/*
 * TowerFireControl.java
 *
 * Created on January 5, 2008, 4:10 PM
 *
 * Copyright (C) 2007-2008  Hylke van der Schaaf
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, in version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package hextd.tower;

import hextd.hex.Hex;
import hextd.creep.Creep;
import hextd.core.Context;
import hextd.core.ContextNormal;
import hextd.util.Geometry;
import hextd.util.ParameterUtils;
import hextd.util.TowerListener;
import hextd.util.WaveStartListener;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.Line2D;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.ArrayList;

/**
 *
 * @author hylke
 */
public class TowerFireControl extends TowerBase implements TowerListener, WaveStartListener {

	public static int price = 0;
	public static int damage = 0;
	public static float range = 2.5f;
	private Color line2Color;
	private Color transColor;
	private int mode1 = 0;
	private int mode2 = 0;
	//
	public static float upgradeEffectRange = 0.5f;
	public static float upgradeEffectDamage = 0.5f;
	//
	private ArrayList<Tower> clients;
	private Line2D[] clientLines;
	private Stroke clientStroke;
	private Color clientColor;
	private HashMap<Tower, EnemyData> targetTable;
	private EnemyData[] enemyData;
	private CompareCloseSame compCloseSame = new CompareCloseSame();
	private CompareCloseDiff compCloseDiff = new CompareCloseDiff();
	private CompareHardSame compHardSame = new CompareHardSame();
	private CompareHardDiff compHardDiff = new CompareHardDiff();
	private CompareWeakSame compWeakSame = new CompareWeakSame();
	private CompareWeakDiff compWeakDiff = new CompareWeakDiff();
	private CompareFarSame compFarSame = new CompareFarSame();
	private CompareFarDiff compFarDiff = new CompareFarDiff();
	String[] modes2 = {"Same", "Diff", "Diff++"};
	String[] modes1 = {"Close", "Hard", "Weak", "Furthest"};

	private class EnemyData {

		public Creep enemy;
		public float dist2;
		public float health;
		public float progress;
		public int towerCount;
		public int nr;

		public EnemyData(Creep e, int i) {
			this.enemy = e;
			this.nr = i;
		}
	}

	private class CompareCloseSame implements Comparator<EnemyData> {

		Comparator<Creep> compare = new CreepCompare.CompareWeak();

		@Override
		public int compare(EnemyData e1, EnemyData e2) {
			if (e1.towerCount != e2.towerCount) {
				return e2.towerCount - e1.towerCount;
			} else {
				if (e1.dist2 != e2.dist2) {
					return Float.compare(e1.dist2, e2.dist2);
				} else {
					return compare.compare(e1.enemy, e2.enemy);
				}
			}
		}
	}

	private class CompareCloseDiff implements Comparator<EnemyData> {

		Comparator<Creep> compare = new CreepCompare.CompareWeak();

		@Override
		public int compare(EnemyData e1, EnemyData e2) {
			if (e1.towerCount != e2.towerCount) {
				return e1.towerCount - e2.towerCount;
			} else {
				if (e1.dist2 != e2.dist2) {
					return Float.compare(e1.dist2, e2.dist2);
				} else {
					return compare.compare(e1.enemy, e2.enemy);
				}
			}
		}
	}

	private class CompareHardSame implements Comparator<EnemyData> {

		Comparator<Creep> compare = new CreepCompare.CompareHard();

		@Override
		public int compare(EnemyData e1, EnemyData e2) {
			if (e1.towerCount != e2.towerCount) {
				return e2.towerCount - e1.towerCount;
			} else {
				return compare.compare(e1.enemy, e2.enemy);
			}
		}
	}

	private class CompareHardDiff implements Comparator<EnemyData> {

		Comparator<Creep> compare = new CreepCompare.CompareHard();

		@Override
		public int compare(EnemyData e1, EnemyData e2) {
			if (e1.towerCount != e2.towerCount) {
				return e1.towerCount - e2.towerCount;
			} else {
				return compare.compare(e1.enemy, e2.enemy);
			}
		}
	}

	private class CompareWeakSame implements Comparator<EnemyData> {

		Comparator<Creep> compare = new CreepCompare.CompareWeak();

		@Override
		public int compare(EnemyData e1, EnemyData e2) {
			if (e1.towerCount != e2.towerCount) {
				return e2.towerCount - e1.towerCount;
			} else {
				return compare.compare(e1.enemy, e2.enemy);
			}
		}
	}

	private class CompareWeakDiff implements Comparator<EnemyData> {

		Comparator<Creep> compare = new CreepCompare.CompareWeak();

		@Override
		public int compare(EnemyData e1, EnemyData e2) {
			if (e1.towerCount != e2.towerCount) {
				return e1.towerCount - e2.towerCount;
			} else {
				return compare.compare(e1.enemy, e2.enemy);
			}
		}
	}

	private class CompareFarSame implements Comparator<EnemyData> {

		Comparator<Creep> compare = new CreepCompare.CompareFurthest();

		@Override
		public int compare(EnemyData e1, EnemyData e2) {
			if (e1.towerCount != e2.towerCount) {
				return e2.towerCount - e1.towerCount;
			} else {
				return compare.compare(e2.enemy, e1.enemy);
			}
		}
	}

	private class CompareFarDiff implements Comparator<EnemyData> {

		Comparator<Creep> compare = new CreepCompare.CompareFurthest();

		@Override
		public int compare(EnemyData e1, EnemyData e2) {
			if (e1.towerCount != e2.towerCount) {
				return e1.towerCount - e2.towerCount;
			} else {
				return compare.compare(e2.enemy, e1.enemy);
			}
		}
	}

	/** Creates a new instance of TowerFireControl */
	public TowerFireControl(Context context, Hex hex, int owner, int activateTick) {
		super(TowerFactory.type.firecontrol, price, damage, range, owner, activateTick);
		this.lineColor = new Color(200, 200, 200);
		this.line2Color = new Color(255, 200, 200);
		this.clientColor = new Color(255, 200, 200);
		this.transColor = new Color(this.lineColor.getRed(), this.lineColor.getGreen(), this.lineColor.getBlue(), 100);
		this.clients = new ArrayList<Tower>();

		this.doInit(context, hex);

		this.context.addTowerListener(this);
		this.context.addWaveStartListener(this);
		this.targetTable = new HashMap<Tower, EnemyData>();
		this.enemyData = new EnemyData[0];

		this.scanTowers();
		this.waveStarted();
	}

	@Override
	protected void makeButtons() {
		if (context instanceof ContextNormal) {
			PanelTowerSettingsDouble p = new PanelTowerSettingsDouble(this, (ContextNormal) context);
			p.setModes(1, this.modes1);
			p.setMode(1, mode1, true);
			p.setModes(2, this.modes2);
			p.setMode(2, this.mode2, true);
			pts = p;
		}
	}

	@Override
	protected boolean runActivate() {
		if (super.runActivate()) {
			scanTowers();
			return true;
		} else {
			return false;
		}
	}

	private void scanTowers() {
		if (!selling && active) {
			Tower t;
			float dx, dy;
			float[] loc;
			for (Iterator<Tower> i = context.getTowers().iterator(); i.hasNext();) {
				t = i.next();
				if (!clients.contains(t) && t.getOwner() == getOwner()) {
					switch (t.getType()) {
						case firecontrol:
						case upgradeDamage:
						case upgradeRange:
							break;
						default:
							loc = t.getHex().getFloatLocation();
							dx = xFloat - loc[0];
							dy = yFloat - loc[1];
							if ((dx * dx + dy * dy) < rangeCurrent2) {
								t.registerTower(this);
							}
							break;
					}
				} else {
					loc = t.getHex().getFloatLocation();
					dx = this.xFloat - loc[0];
					dy = this.yFloat - loc[1];
					if (clients.contains(t) && (dx * dx + dy * dy) > this.rangeCurrent2) {
						t.unregisterTower(this);
					}
				}
			}
		}
	}

	@Override
	protected void calcDamageRange() {
		super.calcDamageRange();
		this.scanTowers();
	}

	@Override
	public boolean canUpgrade() {
		return false;
	}

	@Override
	public String getStatusString() {
		String retString = "";
		retString += "<h1>Fire Control</h1>" +
				"<p>" +
				"Controls the targeting of other towers and adds 25% damage and range.<br>" +
				"<b>Towers controlled:</b> " + this.clients.size() + "<br>\n" +
				"<b>Range:</b> " + this.rangeText + "<br>" +
				"</p>" +
				"<p><b>R</b>: Next control mode</p>\n";
		return retString;
	}

	@Override
	public String getInfoString() {
		String retString = "";
		retString += "<h1>Fire Control</h1>" +
				"<p>" +
				"This tower controls the targeting of other towers and can force them to fire at the same, or different targets. " +
				"Additionally, it increases damage and range of controlled towers  by " +
				upgradeEffectDamage + " and  by " + upgradeEffectRange + "" +
				"It does not contol Blue Towers 1 & 2." +
				"</p><p class='warn'>This tower costs 1 bonus point.</p>";
		return retString;
	}

	@Override
	public void runTick(int gameTime) {
	}

	@Override
	public void towerBuild(Tower t) {
		if (t != this && !selling && !clients.contains(t) && t.getOwner() == getOwner()) {
			switch (t.getType()) {
				case firecontrol:
				case upgradeDamage:
				case upgradeRange:
					break;
				default:
					float dx;
					float dy;
					float[] loc;
					loc = t.getHex().getFloatLocation();
					dx = xFloat - loc[0];
					dy = yFloat - loc[1];
					if ((dx * dx + dy * dy) < rangeCurrent2) {
						t.registerTower(this);
					}
					break;
			}
		}
	}

	@Override
	public void towerRemoved(Tower t) {
		if (this.clients.contains(t)) {
			t.unregisterTower(this);
		}
	}

	public void addClient(Tower t) {
		if (!this.selling && !this.clients.contains(t)) {
			this.clients.add(t);
			this.generateShapes();
		}
	}

	public void removeClient(Tower t) {
		this.clients.remove(t);
		TowerFireControl.EnemyData ed = this.targetTable.get(t);
		if (ed != null) {
			ed.towerCount--;
			this.targetTable.remove(t);
		}
		this.generateShapes();
	}

	@Override
	public void generateShapes() {
		int shapeCount = 13;
		int i;

		float cx = +this.paintScale * 1 / 4;
		float cy = -this.paintScale / 5;
		float[][] points1 = Geometry.createHexCoords(this.paintScale / 2, cx, cy);
		float[][] points2 = Geometry.createHexCoords(this.paintScale);

		i = 0;
		this.shapes = new Shape[shapeCount];
		this.shapes[i++] = new Line2D.Float(cx, cy, points1[0][0], points1[0][1]);
		this.shapes[i++] = new Line2D.Float(cx, cy, points1[1][0], points1[1][1]);
		this.shapes[i++] = new Line2D.Float(cx, cy, points1[2][0], points1[2][1]);
		this.shapes[i++] = new Line2D.Float(cx, cy, points1[3][0], points1[3][1]);
		this.shapes[i++] = new Line2D.Float(cx, cy, points1[4][0], points1[4][1]);
		this.shapes[i++] = new Line2D.Float(cx, cy, points1[5][0], points1[5][1]);
		this.shapes[i++] = new Line2D.Float(cx, cy, points2[0][0], points2[0][1]);
		this.shapes[i++] = new Line2D.Float(cx, cy, points2[1][0], points2[1][1]);
		this.shapes[i++] = new Line2D.Float(cx, cy, points2[2][0], points2[2][1]);
		this.shapes[i++] = new Line2D.Float(cx, cy, points2[3][0], points2[3][1]);
		this.shapes[i++] = new Line2D.Float(cx, cy, points2[5][0], points2[5][1]);
		this.shapes[i++] = Geometry.createCircle(cx, cy, this.paintScale / 2);
		this.shapes[i++] = Geometry.createCircle(cx, cy, this.paintScale / 2);

		this.shapeAction = new int[shapeCount];
		for (i = 0; i < shapeCount; i++) {
			this.shapeAction[i] = 1;
		}
		this.shapeAction[11] = 0;

		this.shapeColor = new Color[shapeCount];
		for (i = 0; i < 6; i++) {
			this.shapeColor[i] = this.line2Color;
		}
		for (i = 6; i < shapeCount; i++) {
			this.shapeColor[i] = this.lineColor;
		}
		this.shapeColor[11] = this.transColor;

		i = 0;
		this.strokes = new Stroke[shapeCount];
		this.strokes[i++] = new BasicStroke(1.5f * strokeScale, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
		this.strokes[i++] = this.strokes[0];
		this.strokes[i++] = this.strokes[0];
		this.strokes[i++] = this.strokes[0];
		this.strokes[i++] = this.strokes[0];
		this.strokes[i++] = this.strokes[0];
		this.strokes[i++] = new BasicStroke(1.0f * strokeScale, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
		this.strokes[i++] = this.strokes[6];
		this.strokes[i++] = this.strokes[6];
		this.strokes[i++] = this.strokes[6];
		this.strokes[i++] = this.strokes[6];
		this.strokes[i++] = this.strokes[6];
		this.strokes[i++] = this.strokes[6];

		this.clientLines = new Line2D[this.clients.size()];
		float[] from;
		float[] to = this.getHex().getPaintLocation();
		to[0] += cx;
		to[1] += cy;
		for (i = 0; i < this.clientLines.length; i++) {
			from = this.clients.get(i).getHex().getPaintLocation();
			this.clientLines[i] = new Line2D.Float(from[0], from[1], to[0], to[1]);
		}
		this.clientStroke = new BasicStroke(1.0f * strokeScale, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
	}

	@Override
	public void paintEffects(Graphics2D g2, int gameTime) {
		if (selected) {
			g2.setStroke(this.clientStroke);
			g2.setColor(this.clientColor);
			for (int i = 0; i < this.clientLines.length; i++) {
				g2.draw(this.clientLines[i]);
			}
		}
	}

	@Override
	public void doCleanup() {
		super.doCleanup();
		for (int i = this.clients.size() - 1; i >= 0; i--) {
			Tower t = this.clients.get(i);
			t.unregisterTower(this);
		}
		this.context.removeTowerListener(this);
		this.context.removeWaveStartListener(this);
	}

	@Override
	public void waveStarted() {
		Collection<Creep> targets = this.context.getTargets(this.getOwner());
		this.enemyData = new EnemyData[targets.size()];
		int i = 0;
		for (Creep e : targets) {
			this.enemyData[i] = new EnemyData(e, i);
			i++;
		}
		this.targetTable.clear();
	}

	@Override
	protected boolean runMode(int m, int v) {
		if (m == 1 && this.mode1 != v) {
			ParameterUtils.forceIndexBounds(modes1, v);
			mode1 = v;

			retarget = true;
			System.out.println("TowerFireControl::setMode: Setting to " + modes1[mode1]);
			if (pts != null && pts.getMode(m) != v) {
				pts.setMode(m, v, true);
			}
			return true;
		} else if (m == 2 && this.mode2 != v) {
			ParameterUtils.forceIndexBounds(modes2, v);
			mode2 = v;

			retarget = true;
			System.out.println("TowerFireControl::setMode: Setting to " + modes2[mode2]);
			if (pts != null && pts.getMode(m) != v) {
				pts.setMode(m, v, true);
			}
			return true;
		} else if (m != 1 && m != 2) {
			System.err.println("TowerFireControl::setMode: unknown mode: " + m);
			return false;
		}
		return true;
	}

	@Override
	public int getMode(int m) {
		if (m == 1) {
			return this.mode1;
		} else if (m == 2) {
			return this.mode2;
		} else {
			System.err.println("TowerBase::setMode: unknown mode: " + m);
			return -1;
		}
	}

	public void updateEnemies(float x, float y) {
		float dx, dy;
		for (int i = 0; i < this.enemyData.length; i++) {
			dx = x - this.enemyData[i].enemy.getX();
			dy = y - this.enemyData[i].enemy.getY();
			enemyData[i].dist2 = dx * dx + dy * dy;
			enemyData[i].health = enemyData[i].enemy.getHealth();
			enemyData[i].progress = enemyData[i].enemy.getProgress();
		}
	}

	public Creep findEnemy(Tower t, Creep preferedEnemy, boolean exShielded) {
		if (this.retarget) {
			preferedEnemy = null;
			this.retarget = false;
		}
		switch (this.mode1) {
			case 0:
				return this.findEnemyClosest(t, preferedEnemy, exShielded);
			case 1:
				return this.findEnemyHardest(t, preferedEnemy, exShielded);
			case 2:
				return this.findEnemyWeakest(t, preferedEnemy, exShielded);
			case 3:
				return this.findEnemyFurthest(t, preferedEnemy, exShielded);
			default:
				System.err.println("TowerFireControl::FindEnemy: Unknown mode1 " + this.mode1);
		}
		return null;
	}

	private Creep findEnemyClosest(Tower t, Creep preferedEnemy, boolean exShielded) {
		return findEnemy(t, preferedEnemy, exShielded, this.compCloseSame, this.compCloseDiff);
	}

	private Creep findEnemyHardest(Tower t, Creep preferedEnemy, boolean exShielded) {
		return findEnemy(t, preferedEnemy, exShielded, this.compHardSame, this.compHardDiff);
	}

	private Creep findEnemyWeakest(Tower t, Creep preferedEnemy, boolean exShielded) {
		return findEnemy(t, preferedEnemy, exShielded, this.compWeakSame, this.compWeakDiff);
	}

	private Creep findEnemyFurthest(Tower t, Creep preferedEnemy, boolean exShielded) {
		return findEnemy(t, preferedEnemy, exShielded, this.compFarSame, this.compFarDiff);
	}

	private Creep findEnemy(Tower t, Creep preferedEnemy, boolean exShielded, Comparator<EnemyData> cSame, Comparator<EnemyData> cDiff) {
		float r2 = t.getRangeCurrent();
		r2 *= r2;
		float[] loc = t.getHex().getFloatLocation();
		this.updateEnemies(loc[0], loc[1]);

		boolean force = false;
		Creep e;
		EnemyData ed, oldEd;
		oldEd = this.targetTable.get(t);

		switch (this.mode2) {
			case 0:
				Arrays.sort(this.enemyData, cSame);
				break;
			case 2:
				force = true;
			case 1:
				if ((oldEd != null) && (oldEd.dist2 < r2) && (oldEd.towerCount == 1)) {
					if (oldEd.enemy.validTarget() && !(exShielded && oldEd.enemy.isShielded())) {
						return oldEd.enemy;
					} else {
						oldEd.towerCount--;
						this.targetTable.remove(t);
						oldEd = null;
					}
				}
				Arrays.sort(this.enemyData, cDiff);
				break;
		}

		for (int i = 0; i < this.enemyData.length; i++) {
			ed = this.enemyData[i];
			e = ed.enemy;
			if (e.validTarget() && !(exShielded && e.isShielded())) {
				if (ed.dist2 < r2 && !(force && ed.towerCount > 0)) {
					// this is our target!
					if (ed != oldEd) {
						// we changed targets!
						if (oldEd != null) {
							oldEd.towerCount--;
						}
						ed.towerCount++;
						this.targetTable.put(t, ed);
					}
					return ed.enemy;
				}
			}
		}
		if (oldEd != null) {
			oldEd.towerCount--;
			this.targetTable.remove(t);
		}
		return null;
	}
}
